﻿using System;
using System.Diagnostics;
using System.IO;

namespace SfsTools.Core
{
    /// <summary>
    /// Implements a stream that reads from and writes to a <see cref="File"/>.
    /// </summary>
    internal class FileStream : Stream, IDisposable
    {
        #region Properties

        /// <summary>
        /// Contains the file to read from and write to.
        /// </summary>
        private File FFile;
        /// <summary>
        /// Contains the access mode.
        /// </summary>
        private readonly FileAccess FAccess;
        /// <summary>
        /// Contains the current position into the file.
        /// </summary>
        private Int64 FPosition;

        #region Inherited

        /// <summary>
        /// Gets a value indicating whether the current stream supports reading.
        /// </summary>
        /// <value><see langword="true"/> if the stream supports reading, <see langword="false"/> otherwise.</value>
        public override Boolean CanRead
        {
            [DebuggerStepThrough]
            get
            {
                if (FFile == null)
                    throw new ObjectDisposedException(null);
                return (FAccess & FileAccess.Read) != 0;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the current stream supports seeking.
        /// </summary>
        /// <value><see langword="true"/> if the stream supports seeking, <see langword="false"/> otherwise.</value>
        public override Boolean CanSeek
        {
            [DebuggerStepThrough]
            get
            {
                if (FFile == null)
                    throw new ObjectDisposedException(null);
                // Always can seek.
                return true;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the current stream supports writing.
        /// </summary>
        /// <value><see langword="true"/> if the stream supports writing, <see langword="false"/> otherwise.</value>
        public override Boolean CanWrite
        {
            [DebuggerStepThrough]
            get
            {
                if (FFile == null)
                    throw new ObjectDisposedException(null);
                return (FAccess & FileAccess.Write) != 0;
            }
        }

        /// <summary>
        /// Gets the length in bytes of the stream.
        /// </summary>
        /// <exception cref="NotSupportedException">The stream does not support seeking.</exception>
        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
        public override Int64 Length
        {
            [DebuggerStepThrough]
            get
            {
                if (FFile == null)
                    throw new ObjectDisposedException(null);
                return FFile.Length;
            }
        }

        /// <summary>
        /// Gets or sets the position within the current stream.
        /// </summary>
        /// <exception cref="IOException">An I/O error occured.</exception>
        /// <exception cref="NotSupportedException">A class derived from <see cref="Stream"/> does not support seeking.</exception>
        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
        public override Int64 Position
        {
            [DebuggerStepThrough]
            get
            {
                if (FFile == null)
                    throw new ObjectDisposedException(null);
                return FPosition;
            }
            [DebuggerStepThrough]
            set
            {
                if (FFile == null)
                    throw new ObjectDisposedException(null);
                Seek(value, SeekOrigin.Begin);
            }
        }

        #endregion

        #endregion

        #region Methods

        #region Inherited

        /// <summary>
        /// Closes the stream and releases resources.
        /// </summary>
        [DebuggerStepThrough]
        public override void Close()
        {
            Dispose(true);
        }

        /// <summary>
        /// Releases the unmanaged resources used by the <see cref="Stream"/> and optionally releases the managed resources.
        /// </summary>
        /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.</param>
        [DebuggerStepThrough]
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (FFile != null)
                {
                    // Update timestamp of file if opened for writing.
                    if ((FAccess & FileAccess.Write) != 0)
                    {
                        FFile.LastUpdate = TimeStamp.Now;
                    }
                    FFile.Close();
                    FFile = null;
                }
            }
            base.Dispose(disposing);
        }

        /// <summary>
        /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device.
        /// </summary>
        /// <exception cref="IOException">An I/O error occured.</exception>
        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
        [DebuggerStepThrough]
        public override void Flush()
        {
            if (FFile == null)
                throw new ObjectDisposedException(null);
            // No-op.
        }

        /// <summary>
        /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read.
        /// </summary>
        /// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between <paramref name="index"/> and (<paramref name="index"/> + <paramref name="count"/> - 1) replaced by the bytes read from the current source.</param>
        /// <param name="index">The zero-based byte offset in <paramref name="buffer"/> at which to begin storing the data read from the current stream.</param>
        /// <param name="count">The maximum number of bytes to be read from the current stream.</param>
        /// <returns>The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached.</returns>
        /// <exception cref="ArgumentException">The sum of <paramref name="index"/> and <paramref name="count"/> is larger than the buffer length.</exception>
        /// <exception cref="ArgumentNullException">The argument <paramref name="buffer"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentOutOfRangeException">The argument <paramref name="index"/> or <paramref name="count"/> is negative.</exception>
        /// <exception cref="IOException">An I/O error occured.</exception>
        /// <exception cref="NotSupportedException">A class derived from <see cref="Stream"/> does not support reading.</exception>
        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
        [DebuggerStepThrough]
        public override int Read(byte[] buffer, int index, int count)
        {
            if (FFile == null)
                throw new ObjectDisposedException(null);
            if ((FAccess & FileAccess.Read) == 0)
                throw new NotSupportedException();
            int read = FFile.Read(FPosition, buffer, index, count);
            FPosition += read;
            return read;
        }

        /// <summary>
        /// Sets the position within the current stream.
        /// </summary>
        /// <param name="offset">A byte offset relative to the <paramref name="origin"/> parameter.</param>
        /// <param name="origin">A value of type <see cref="System.IO.SeekOrigin"/> indicating the reference point used to obtain the new position.</param>
        /// <returns>The new position within the current stream.</returns>
        /// <exception cref="IOException">An I/O error occured.</exception>
        /// <exception cref="NotSupportedException">A class derived from <see cref="Stream"/> does not support seeking.</exception>
        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
        [DebuggerStepThrough]
        public override Int64 Seek(Int64 offset, SeekOrigin origin)
        {
            if (FFile == null)
                throw new ObjectDisposedException(null);
            long length = FFile.Length;
            switch (origin)
            {
                case SeekOrigin.Begin:
                    {
                        break;
                    }
                case SeekOrigin.Current:
                    {
                        offset = FPosition + offset;
                        break;
                    }
                case SeekOrigin.End:
                    {
                        offset = length + offset;
                        break;
                    }
                default:
                    {
                        throw new ArgumentException("", "origin");
                    }
            }
            if ((offset < 0) || (offset > length))
                throw new InvalidSeekPositionException();
            return FPosition = offset;
        }

        /// <summary>
        /// Sets the length of the current stream.
        /// </summary>
        /// <param name="value">The desired length of the current stream in bytes.</param>
        /// <exception cref="IOException">An I/O error occured.</exception>
        /// <exception cref="NotSupportedException">A class derived from <see cref="Stream"/> does not support seeking.</exception>
        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
        [DebuggerStepThrough]
        public override void SetLength(long value)
        {
            if (FFile == null)
                throw new ObjectDisposedException(null);
            FFile.SetLength(value);
        }

        /// <summary>
        /// Writes a sequence of bytes to the current stream and advances the position within the stream by the number of bytes written.
        /// </summary>
        /// <param name="buffer">An array of bytes. This method copies count bytes from buffer to the current stream.</param>
        /// <param name="index">The zero-based byte offset in <paramref name="buffer"/> at which to begin copying bytes to the current stream.</param>
        /// <param name="count">The number of bytes to be written to the current stream.</param>
        /// <exception cref="ArgumentException">The sum of <paramref name="index"/> and <paramref name="count"/> is larger than the buffer length.</exception>
        /// <exception cref="ArgumentNullException">The argument <paramref name="buffer"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentOutOfRangeException">The argument <paramref name="index"/> or <paramref name="count"/> is negative.</exception>
        /// <exception cref="IOException">An I/O error occured.</exception>
        /// <exception cref="NotSupportedException">A class derived from <see cref="Stream"/> does not support writing.</exception>
        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
        [DebuggerStepThrough]
        public override void Write(byte[] buffer, int index, int count)
        {
            if (FFile == null)
                throw new ObjectDisposedException(null);
            if ((FAccess & FileAccess.Write) == 0)
                throw new NotSupportedException();
            FFile.Write(FPosition, buffer, index, count);
            FPosition += count;
        }

        #endregion

        #endregion

        #region Constructors
        /// <overloads>Initializes a new instance of <see cref="FileStream"/>.</overloads>
        /// <summary>
        /// Initializes a new instance of <see cref="FileStream"/>.
        /// </summary>
        /// <param name="file">The <see cref="File"/> instance to read from or write to.</param>
        /// <param name="access">The access allowed to the file.</param>
        /// <exception cref="ArgumentNullException">The argument <paramref name="file"/> is <see langword="null"/>.</exception>
        [DebuggerStepThrough]
        public FileStream(File file, FileAccess access)
            : base()
        {
            if (file == null)
                throw new ArgumentNullException("file");
            FFile = file;
            FAccess = access;
            FFile.Open();
        }
        #endregion

        #region IDisposable Members

        /// <summary>
        /// Releases all resources used by this <see cref="FileStream"/> instance.
        /// </summary>
        [DebuggerStepThrough]
        void IDisposable.Dispose()
        {
            Dispose(true);
        }

        #endregion
    }
}
